package weeny.pre.util;

import static prefuse.visual.VisualItem.X;
import static prefuse.visual.VisualItem.Y;
import static weeny.pre.util.Pre.src;
import static weeny.pre.util.Pre.tar;

import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;

import prefuse.data.Edge;
import prefuse.data.Tuple;
import prefuse.visual.EdgeItem;
import prefuse.visual.VisualItem;
import weeny.util.logs.Debug;

/**
 * Some helper functions, to help you dealing with Points/Node/NodeItem and
 * Line/Edge/EdgeItem, and treating them as the same.
 * 
 * Basically there are several categories of functions
 * 1) dumpPnt/toPoint functions
 * 2) dumpLine/toLine functions
 * 3) point and line relation functions
 * 4) angle functions
 * 5) line/seg intersection functions
 * 
 * @author weeny
 */

public class Geom {
	//straight line intersection type
	public static enum TYPE{ COINCIDENT, PARALLEL, INTERSECT; }
	
	//helper constant variables
	public static final Point2D PNT_ZERO = new Point2D.Double(0,0);
	public static final double HALF_PI = Math.PI/2;
	
	//private helper variables/functions
	private static Point2D m_asPntA = new Point2D.Double();
	private static Point2D m_asPntB = new Point2D.Double();
	private static Point2D m_asPntC = new Point2D.Double();
	private static Point2D asPntA(Object obj){
		return Geom.dumpPnt(obj, m_asPntA);
	}
	private static Point2D asPntB(Object obj){
		return Geom.dumpPnt(obj, m_asPntB);
	}
	private static Point2D asPntC(Object obj){
		return Geom.dumpPnt(obj, m_asPntC);
	}
	
	/**
	 * Give the X coordinate of the input (VisualItem or Point or Tuple)
	 */
	public static double X(Object o){
		if (o instanceof VisualItem) {
			return ((VisualItem) o).getX();
		}
		else if (o instanceof Point2D){
			return ((Point2D) o).getX();
		}
		else{
			return ((Tuple)o).getDouble(X);
		}
	}

	/**
	 * Give the Y coordinate of the input (VisualItem or Point or Tuple)
	 */
	public static double Y(Object o){
		if (o instanceof VisualItem) {
			return ((VisualItem) o).getY();
		}
		else if (o instanceof Point2D){
			return ((Point2D) o).getY();
		}
		else{
			return ((Tuple)o).getDouble(Y);
		}
	}
	
	/**
	 * Set the coordinates of pnt with X(obj) and Y(obj)
	 */
	public static Point2D dumpPnt(Object obj, Point2D pnt){
		pnt.setLocation(X(obj), Y(obj));
		return pnt;
	}
	
	/**
	 * Create a new Point containing the position of obj
	 */
	public static Point2D toPnt(Object obj){
		return new Point2D.Double(X(obj),Y(obj));
	}
	
	/**
	 * Calculate the position between src and tar with:
	 *   (X(b)-X(p))/(X(b)-X(a)) = (Y(b)-Y(p))/(Y(b)-Y(a)) = frac
	 */
	public static Point2D toPntFracBetween(Object src, double frac, Object tar){
		Point2D a = asPntA(src);
		Point2D b = asPntB(tar);
		return new Point2D.Double(X(a)+(X(b)-X(a))*frac, Y(a)+(Y(b)-Y(a))*frac);
	}
	/**
	 * Give the value of (X(obj1)+X(obj2), Y(obj1)+Y(obj2))
	 */
	public static Point2D toPntAdd(Object obj1, Object obj2){
		return Geom.toPntMultiAdd(obj1, obj2, 1);
	}
	/**
	 * Give the value of (X(obj1)-X(obj2), Y(obj1)-Y(obj2))
	 */
	public static Point2D toPntMinus(Object obj1, Object obj2){
		return Geom.toPntMultiAdd(obj1, obj2, -1);
	}
	/**
	 * Give the value of (X(src)+X(speed)*times, Y(src)+Y(speed)*times)
	 */
	public static Point2D toPntMultiAdd(Object src, Object speed, double times){
		Point2D a = asPntA(src);
		Point2D b = asPntB(speed);
		return new Point2D.Double(X(a)+X(b)*times, Y(a)+Y(b)*times);
	}
	/**
	 * Give the value of (X(speed)*times, Y(speed)*times)
	 */
	public static Point2D toPntMulti(Object speed, double times){
		return toPntMultiAdd(PNT_ZERO, speed, times);
	}
	
	//relative position functions
	
	/**
	 * @see Line2D#relativeCCW(Point2D)
	 */
	public static int relativePos(Object line, Object pnt){
		return asLine(line).relativeCCW(asPntA(pnt));
	}
	/**
	 * Give you the distance from the pnt to the infinitely-extended line
	 * @see Line2D#ptLineDist(Point2D)
	 */
	public static double ptLineDist(Object line, Object pnt){
		return asLine(line).ptLineDist(asPntA(pnt));
	}

	private static Line2D asLineA = new Line2D.Double();
	private static Line2D asLineB = new Line2D.Double();

	//asLine functions
	/**
	 * Set the coordinates of line with the info from obj, which could be Line2D
	 * or EdgeItem or Edge.
	 * And we output the passed-in line for convenience purpose.
	 */
	public static Line2D dumpLine(Object obj, Line2D line){
		if (obj instanceof Line2D) {
			Line2D new_name = (Line2D) obj;
			line.setLine(new_name);
			return line;
		}
		else if (obj instanceof EdgeItem) {
			EdgeItem e = (EdgeItem) obj;
			return dumpLine(e.getSourceItem(), e.getTargetItem(), line);
		}
		else{
			Edge e = (Edge) obj;
			return dumpLine(e.getSourceNode(), e.getTargetNode(), line);
		}
	}
	/**
	 * Set the coordinates of the line with (X(src),Y(src), Y(tar), Y(tar)). And
	 * we output the passed-in line for convenience purpose.
	 */
	public static Line2D dumpLine(Object src, Object tar, Line2D line){
		line.setLine(asPntA(src), asPntB(tar));
		return line;
	}
//not safe!
//	public static Line2D asLine(double x1, double y1, double x2, double y2){
//		asLineA.setLine(x1, y1, x2, y2);
//		return asLineA;
//	}
	private static Line2D asLine(Object obj){
		return dumpLine(obj, asLineA);
	}
	/**
	 * Get the squared distance between obj1 and obj2
	 * @see Point2D#distanceSq(Point2D)
	 */
	public static double distSq(Object obj1, Object obj2){
		return asPntA(obj1).distanceSq(asPntB(obj2));
	}
	/**
	 * Get the distance between obj1 and obj2
	 * @see Point2D#distance(Point2D)
	 */
	public static double dist(Object obj1, Object obj2){
		return Math.sqrt(distSq(obj1, obj2));
	}
	
	//toLine Functions
	
	/**
	 * Give you a new instance of Line2D with the info from obj
	 */
	public static Line2D toLine(Object obj){
		Line2D line = new Line2D.Double();
		return dumpLine(obj, line);
	}
	
	//edge/line functions
	
	/**
	 * Get the squared length of the obj
	 * @see Point2D#distanceSq(Point2D)
	 */
	public static double lengthSq(Object obj){
		Line2D line = asLine(obj);
		return line.getP1().distanceSq(line.getP2());
	}
	/**
	 * Get the length of the obj
	 */
	public static double length(Object vi){
		return Math.sqrt(lengthSq(vi));
	}
	//line function
	public static double deltaX(Line2D line){
		return line.getX2()-line.getX1();
	}
	public static double deltaY(Line2D line){
		return line.getY2()-line.getY1();
	}
	public static double deltaX(Object edge){
		return deltaX(asLine(edge));
	}
	public static double deltaY(Object edge){
		return deltaY(asLine(edge));
	}
	public static double srcX(Object obj){
		return asLine(obj).getX1();
	}
	public static double srcY(Object obj){
		return asLine(obj).getY1();
	}
	public static double tarX(Object obj){
		return asLine(obj).getX2();
	}
	public static double tarY(Object obj){
		return asLine(obj).getY2();
	}
	
	public static Point2D middleOf(Object obj){
		Line2D line = asLine(obj);
		return new Point2D.Double(
				line.getX1()/2+line.getX2()/2,
				line.getY1()/2+line.getY2()/2);
	}
	
	//fraction functions
	public static double fracBetween(Object source, Object mid, Object target){
		Point2D a = asPntA(source);
		Point2D b = asPntB(target);
		Point2D m = asPntC(mid);
		//if x's are equal or y's are equal, there are problems!!
		double frac1 = (m.getX()-a.getX()) / (b.getX()-a.getX());
		double frac2 = (m.getY()-a.getY()) / (b.getY()-a.getY());
		if (Math.abs(frac2 - frac1) > 0.01) {
			Debug.msg("May not be on the same line"+Math.abs(frac2 - frac1));
		}
		return frac1;
	}
	public static double fracXBetween(Object source, Object mid, Object target){
		Point2D a = asPntA(source);
		Point2D b = asPntB(target);
		Point2D m = asPntC(mid);
		double result1 = (m.getX()-a.getX()) / (b.getX()-a.getX());
		return result1;
	}
	public static double fracYBetween(Object source, Object mid, Object target){
		Point2D a = asPntA(source);
		Point2D b = asPntB(target);
		Point2D m = asPntC(mid);
		double result1 = (m.getY()-a.getY()) / (b.getY()-a.getY());
		return result1;
	}
	public static void curvePath(GeneralPath gp, Object ctl1, Object ctl2, 
			Object target){
		Point2D a = asPntA(ctl1);
		Point2D b = asPntB(ctl2);
		Point2D c = asPntC(target);
		gp.curveTo((float)a.getX(), (float)a.getY(), 
				   (float)b.getX(), (float)b.getY(), 
				   (float)c.getX(), (float)c.getY());
	}
	
	

	
	////////////////////////////////////////////////////////////////////////////
	//                            Angle Functions                             //
	////////////////////////////////////////////////////////////////////////////
	
	// return -pi to pi (in the screen space)	
	//                  ^
	//   (-pi/2 to -pi) |  (0 to -pi/2)
	//  ----------------+------------->
	//     (pi/2 to pi) |  (0 to pi/2)
	//                  v
	//right-bottom is positive
	//right-top is negtive
	public static double angle(double x1, double y1, double x2, double y2){
		double deltaY = y2-y1;//n2.getY() - n1.getY();
		double deltaX = x2-x1;//n2.getX() - n1.getX();
		double ret = Math.atan(deltaY / deltaX);
		assert !Double.isNaN(ret) : deltaX+","+deltaY;
		if (x2<x1){
			if (ret > 0) {
				ret = ret - Math.PI;
			}
			else
				ret = Math.PI + ret;
		}
		if (ret > Math.PI) {
			return Math.PI;
		}
		else if (ret < -Math.PI){
			return -Math.PI;
		}
		return ret;
	}
	public static double deltaAngle(double angleA, double angleB){
		if (angleA < -Math.PI || angleA > Math.PI || 
			angleB < -Math.PI || angleB > Math.PI) {
			System.out.println("angleA = "+angleA);
			System.out.println("angleB = "+angleB);
			throw new IllegalArgumentException(
					"angle should be in the range of -pi to pi");
		}
		double delta = Math.abs(angleA - angleB);
		if (delta < Geom.HALF_PI) {
			return delta;
		} else if (delta < Math.PI) {
			return Math.PI - delta;
		} else if (delta < Geom.HALF_PI * 3) {
			return delta - Math.PI;
		}
		return Math.PI * 2 - delta;
	}

	public static double cosValue(Object o1, Object com, Object o2){
		double p1p2Sq = asPntA(o1).distanceSq(asPntB(com));
		double p2p3Sq = asPntA(com).distanceSq(asPntB(o2));
		double p1p3Sq = asPntA(o1).distanceSq(asPntB(o2));
		double p1p2 = Math.sqrt(p1p2Sq);
		double p2p3 = Math.sqrt(p2p3Sq);
		double ret = (p1p2Sq + p2p3Sq - p1p3Sq) / (2.0 * p1p2 * p2p3);
		if (Math.abs(ret) < 1) {
			return ret;
		}
		else if (Math.abs(Math.abs(ret)-1)<0.00001){
			return ret<0? -1:1;
		}
		else{
			throw new IllegalStateException("cosValue is illegal ="+ret);
		}
	}
	public static double angle(Object o){
		if (o instanceof Edge) {
			Edge e = (Edge) o;
			return angle(asPntA(e.getSourceNode()), asPntB(e.getTargetNode()));
		}
		else{
			Line2D l = (Line2D) o;
			return angle(l.getX1(), l.getY1(), l.getX2(), l.getY2());
		}
	}
	public static double angle(Point2D n1, Point2D n2){
		return angle(n1.getX(), n1.getY(), n2.getX(), n2.getY());
	}
	public static double angle(VisualItem n1, VisualItem n2){
		return angle(n1.getX(), n1.getY(), n2.getX(), n2.getY());
	}
	
	
	public static TYPE lineIntersects(double x1, double y1, double x2, 
			double y2, double x3, double y3, double x4, double y4, Point2D p) {
		double ua_t = (x4-x3)*(y1-y3)-(y4-y3)*(x1-x3);
		double ub_t = (x2-x1)*(y1-y3)-(y2-y1)*(x1-x3);
        double u_b  = (y4-y3)*(x2-x1)-(x4-x3)*(y2-y1);

        if ( u_b != 0 ) {
            double ua = ua_t / u_b;
            p.setLocation(x1+ua*(x2-x1), y1+ua*(y2-y1));
            return TYPE.INTERSECT;
        }
        else {
        	return ua_t * ub_t == 0? TYPE.COINCIDENT : TYPE.PARALLEL;
        }
	}
	
	/**
	 * Get the position of the intersection.
	 * @return null if they are 
	 */
	public static Point2D lineIntersection(
			Object a1, Object b1, Object a2, Object b2) {
		Point2D result = new Point2D.Double();
		boolean flag = lineIntersects(a1, b1, a2, b2, result);
		return flag? result : null;
	}
	public static boolean lineIntersects(
			Object a1, Object b1, Object a2, Object b2) {
		return lineIntersects(a1, b1, a2, b2, null);
	}
	public static boolean lineIntersects(
			Object a1, Object b1, Object a2, Object b2, Point2D result) {
		TYPE type = lineIntersects(X(a1), Y(a1), X(b1), Y(b1), 
				                   X(a2), Y(a2), X(b2), Y(b2), result);
		return type == TYPE.INTERSECT;
	}
	public static boolean lineIntersects(Object o1, Object o2){
		return lineIntersects(o1, o2, null);
	}
	public static boolean lineIntersects(Object o1, Object o2, Point2D result){
		Line2D lne1 = dumpLine(o1, asLineA);
		Line2D lne2 = dumpLine(o2, asLineB);
		TYPE type = lineIntersects(
				lne1.getX1(), lne1.getY1(), lne1.getX2(), lne1.getY2(),
				lne2.getX1(), lne2.getY1(), lne2.getX2(), lne2.getY2(), result);
		return type == TYPE.INTERSECT;
	}
	public static boolean segIntersects(Object edge1, Object edge2){
		return dumpLine(edge1, asLineA).intersectsLine(dumpLine(edge2,asLineB));
	}
	public static boolean segIntersects(
			Object src1, Object tar1, Object src2, Object tar2){
		Line2D seg1 = dumpLine(src1, tar1, asLineA);
		Line2D seg2 = dumpLine(src2, tar2, asLineB);
		return seg1.intersectsLine(seg2);
	}
	public static boolean segIntersects(VisualItem edge1, VisualItem edge2){
		return segIntersects(src(edge1), tar(edge2), src(edge2), tar(edge2));
	}
	public static boolean segIntersects(VisualItem edge,Point2D p1,Point2D p2){
		return segIntersects(src(edge), tar(edge), p1, p2);
	}
}